// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:_fe_analyzer_shared/src/metadata/expressions.dart' as shared;
import 'package:_fe_analyzer_shared/src/scanner/scanner.dart' show Token;
import 'package:kernel/ast.dart';
import 'package:kernel/clone.dart';

import '../api_prototype/experimental_flags.dart';
import '../base/extension_scope.dart';
import '../base/loader.dart';
import '../base/scope.dart' show LookupScope;
import '../kernel/body_builder_context.dart';
import '../kernel/macro/metadata.dart' hide ExtensionScope;
import '../source/source_library_builder.dart' show SourceLibraryBuilder;

bool computeSharedExpressionForTesting = false;
bool delaySharedExpressionLookupForTesting = false;

class Annotation {
  final MetadataBuilder metadataBuilder;

  final Token atToken;
  final bool createFileUriExpression;

  Annotation(
    this.metadataBuilder,
    this.atToken, {
    required this.createFileUriExpression,
  });

  late int annotationIndex;
  late Expression expression;
}

class MetadataBuilder {
  /// Token for `@` for annotations that have not yet been parsed.
  Token? _atToken;

  final int atOffset;

  /// `true` if the annotation begins with 'patch'.
  ///
  /// This is used for detecting `@patch` annotations in patch libraries where
  /// it can be assumed that it implies that it _is_ a `@patch` annotation.
  final bool hasPatch;

  /// Expression for an already parsed annotation.
  Expression? _expression;

  final Uri fileUri;

  MetadataBuilder(Token this._atToken, this.fileUri)
    : atOffset = _atToken.charOffset,
      hasPatch = _atToken.next?.lexeme == 'patch';

  // Coverage-ignore(suite): Not run.
  Token? get beginToken => _atToken;

  shared.Expression? _sharedExpression;

  shared.Expression? _unresolvedSharedExpressionForTesting;

  // Coverage-ignore(suite): Not run.
  shared.Expression? get expression => _sharedExpression;

  // Coverage-ignore(suite): Not run.
  shared.Expression? get unresolvedExpressionForTesting =>
      _unresolvedSharedExpressionForTesting;

  static void buildAnnotations({
    required Annotatable annotatable,
    required Uri annotatableFileUri,
    required List<MetadataBuilder>? metadata,
    required Uri annotationsFileUri,
    required BodyBuilderContext bodyBuilderContext,
    required SourceLibraryBuilder libraryBuilder,
    required ExtensionScope extensionScope,
    required LookupScope scope,
  }) {
    if (metadata == null) return;

    // Cloner used to clone already parsed annotations.
    CloneVisitorNotMembers? cloner;

    List<Annotation> annotations = [];
    for (int i = 0; i < metadata.length; ++i) {
      MetadataBuilder annotationBuilder = metadata[i];
      bool createFileUriExpression =
          annotatableFileUri != annotationBuilder.fileUri;
      Token? beginToken = annotationBuilder._atToken;
      annotationBuilder._atToken = null;
      if (beginToken != null) {
        if (computeSharedExpressionForTesting) {
          // Coverage-ignore-block(suite): Not run.
          annotationBuilder._sharedExpression = _parseSharedExpression(
            libraryBuilder.loader,
            beginToken,
            libraryBuilder.importUri,
            annotationBuilder.fileUri,
            scope,
            libraryBuilder.libraryFeatures,
          );
          if (delaySharedExpressionLookupForTesting) {
            annotationBuilder._unresolvedSharedExpressionForTesting =
                _parseSharedExpression(
                  libraryBuilder.loader,
                  beginToken,
                  libraryBuilder.importUri,
                  annotationBuilder.fileUri,
                  scope,
                  libraryBuilder.libraryFeatures,
                  delayLookupForTesting: true,
                );
          }
        }
        annotations.add(
          new Annotation(
            annotationBuilder,
            beginToken,
            createFileUriExpression: createFileUriExpression,
          ),
        );
      } else {
        // The annotation is needed for multiple declarations so we need to
        // clone the expression to use it more than once. For instance
        //
        //     abstract class Class {
        //       @annotation
        //       abstract int field;
        //     }
        //
        // will be compiled to
        //
        //     abstract class Class {
        //       @annotation
        //       int get field;
        //       @annotation
        //       void set field(int value);
        //     }
        //
        cloner ??= new CloneVisitorNotMembers();
        Expression annotation = cloner.cloneInContext(
          annotationBuilder._expression!,
        );
        // Coverage-ignore(suite): Not run.
        if (createFileUriExpression && annotation is! FileUriExpression) {
          annotation = new FileUriExpression(
            annotation,
            annotationBuilder.fileUri,
          )..fileOffset = annotationBuilder.atOffset;
        }
        annotatable.addAnnotation(annotation);
      }
    }
    libraryBuilder.loader.createResolver().buildAnnotations(
      libraryBuilder: libraryBuilder,
      bodyBuilderContext: bodyBuilderContext,
      annotationsFileUri: annotationsFileUri,
      extensionScope: extensionScope,
      scope: scope,
      annotatable: annotatable,
      annotations: annotations,
    );
    for (Annotation annotation in annotations) {
      annotation.metadataBuilder._expression = annotation.expression;
    }
  }
}

// Coverage-ignore(suite): Not run.
shared.Expression _parseSharedExpression(
  Loader loader,
  Token atToken,
  Uri importUri,
  Uri fileUri,
  LookupScope scope,
  LibraryFeatures libraryFeatures, {
  bool delayLookupForTesting = false,
}) {
  return parseAnnotation(
    loader,
    atToken,
    importUri,
    fileUri,
    scope,
    libraryFeatures,
    delayLookupForTesting: delayLookupForTesting,
  );
}
